package de.lighti.starcraft;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.InetSocketAddress;
import java.net.StandardSocketOptions;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.channels.spi.SelectorProvider;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;

import de.lighti.starcraft.ClientEntry.Status;
import de.lighti.starcraft.network.ByteBufferBackedInputStream;
import de.lighti.starcraft.network.ChangeRequest;
import de.lighti.starcraft.network.Message;

public class Network implements Runnable {
    private final Server server;

    private final static Logger LOGGER = Logger.getLogger( Network.class.getName() );

    // The channel on which we'll accept connections
    private ServerSocketChannel serverChannel;

    // The selector we'll be monitoring
    private Selector selector;

    // A list of ChangeRequest instances
    private final List<ChangeRequest> changeRequests = new LinkedList<ChangeRequest>();

    // The buffer into which we'll read data when it's available
    private final ByteBuffer readBuffer = ByteBuffer.allocate( 8192 );

    private final Map<SocketChannel, ClientEntry> clients;

    // Maps a SocketChannel to a list of ByteBuffer instances
    private final Map<SocketChannel, List<ByteBuffer>> pendingData = new HashMap<SocketChannel, List<ByteBuffer>>();

    private boolean shutdownRequested;

    public Network( Server server ) {
        this.server = server;
        clients = new HashMap<SocketChannel, ClientEntry>();
    }

    private void accept( SelectionKey key ) throws IOException {
        // For an accept to be pending the channel must be a server socket channel.
        final ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();

        // Accept the connection and make it non-blocking
        final SocketChannel socketChannel = serverSocketChannel.accept();
        socketChannel.configureBlocking( false );
        socketChannel.setOption( StandardSocketOptions.SO_KEEPALIVE, true );

        LOGGER.info( "Received new connection from " + socketChannel.getRemoteAddress() );

        // Register the new SocketChannel with our Selector, indicating
        // we'd like to be notified when there's data waiting to be read
        socketChannel.register( selector, SelectionKey.OP_READ );
        final ClientEntry c = new ClientEntry( socketChannel.getRemoteAddress().toString() );
        c.setStatus( Status.AWAITNG_REPLY );
        clients.put( socketChannel, c );
    }

    public void disconnect( ClientEntry client ) {
        // TODO Auto-generated method stub

    }

    private SocketChannel findSocketForClient( ClientEntry client ) {
        for (final Map.Entry<SocketChannel, ClientEntry> e : clients.entrySet()) {
            if (client == e.getValue()) {
                return e.getKey();
            }
        }
        return null;
    }

    public Collection<ClientEntry> getClients() {
        return clients.values();
    }

    public int getClientsReady() {
        int ret = 0;
        for (final ClientEntry c : clients.values()) {
            if (c.getStatus() == Status.READY) {
                ret++;
            }
        }

        return ret;
    }

    public ClientEntry getFreeClient() {
        for (final ClientEntry c : clients.values()) {
            if (c.getStatus() == Status.READY) {
                return c;
            }
        }

        return null;
    }

    private Selector initSelector() throws IOException {
        // Create a new selector
        final Selector socketSelector = SelectorProvider.provider().openSelector();

        // Create a new non-blocking server socket channel
        serverChannel = ServerSocketChannel.open();
        serverChannel.configureBlocking( false );

        // Bind the server socket to the specified port
        final InetSocketAddress isa = new InetSocketAddress( server.getPort() );
        serverChannel.socket().bind( isa );

        // Register the server socket channel, indicating an interest in 
        // accepting new connections
        serverChannel.register( socketSelector, SelectionKey.OP_ACCEPT );

        return socketSelector;
    }

    private void parseMessage( SocketChannel socketChannel, ByteBuffer b ) throws IOException {
        final ObjectInputStream ois = new ObjectInputStream( new ByteBufferBackedInputStream( b ) );

        try {
            final Message m = (Message) ois.readObject();
            LOGGER.fine( socketChannel.getRemoteAddress() + " sent " + m );
            server.handleMessage( clients.get( socketChannel ), m );
        }
        catch (final ClassNotFoundException e) {
            throw new RuntimeException( e );
        }
        finally {
            ois.close();
        }

    }

    private void processData( SocketChannel socketChannel, int numRead ) throws IOException {
        LOGGER.fine( "Received " + numRead + " bytes from " + socketChannel.getRemoteAddress() );
        final ClientEntry client = clients.get( socketChannel );
        readBuffer.limit( numRead );

        while (readBuffer.hasRemaining()) {
            final ByteBuffer clientBuffer = client.getMessageBuffer();
            if (clientBuffer == null) {
                final int messageSize = readBuffer.getInt();
                if (messageSize <= readBuffer.remaining()) {
                    parseMessage( socketChannel, readBuffer );
                }
                else {
                    client.setExpectedMessageSize( messageSize );
                    final ByteBuffer newclientBuffer = ByteBuffer.allocate( messageSize );
                    newclientBuffer.put( readBuffer );
                    client.setMessageBuffer( newclientBuffer );
                }
            }
            else {
                int remainingBytes = client.getExpectedMessageSize() - clientBuffer.position();
                while (remainingBytes > 0 && readBuffer.hasRemaining()) {
                    clientBuffer.put( readBuffer.get() );
                    remainingBytes--;
                }
                if (remainingBytes == 0) {
                    clientBuffer.rewind();
                    parseMessage( socketChannel, clientBuffer );
                    client.setExpectedMessageSize( 0 );
                    client.setMessageBuffer( null );
                }
            }
        }
    }

    private void read( SelectionKey key ) throws IOException {
        final SocketChannel socketChannel = (SocketChannel) key.channel();

        // Clear out our read buffer so it's ready for new data
        readBuffer.clear();

        // Attempt to read off the channel
        int numRead;
        try {
            numRead = socketChannel.read( readBuffer );
        }
        catch (final IOException e) {
            // The remote forcibly closed the connection, cancel
            // the selection key and close the channel.
            key.cancel();
            removeClient( socketChannel );
            return;
        }

        if (numRead == -1) {
            // Remote entity shut the socket down cleanly. Do the
            // same from our end and cancel the channel.
            key.cancel();
            removeClient( socketChannel );

            return;
        }
        else {
            readBuffer.rewind();

            try {
                // Hand the data off to our worker thread
                processData( socketChannel, numRead );
            }
            catch (final IOException e) {
                LOGGER.severe( "Client " + socketChannel.getRemoteAddress() + " caused error: " + e.getLocalizedMessage() );
                e.printStackTrace();
                removeClient( socketChannel );
            }
        }
    }

    private void removeClient( SocketChannel channel ) throws IOException {
        channel.close();
        final ClientEntry c = clients.remove( channel );
        LOGGER.info( c + " disconnected" );
        if (c.getCurrentMatch() != null) {
            server.rescheduleMatch( c.getCurrentMatch() );
        }
    }

    @Override
    public void run() {
        try {
            selector = initSelector();
            LOGGER.info( "Server listening on port " + server.getPort() );
            while (true) {

                // Process any pending changes
                synchronized (changeRequests) {
                    final Iterator<ChangeRequest> changes = changeRequests.iterator();
                    while (changes.hasNext()) {
                        final ChangeRequest change = changes.next();
                        switch (change.type) {
                            case ChangeRequest.CHANGEOPS:
                                final SelectionKey key = change.socket.keyFor( selector );
                                if (key == null) {
                                    changes.remove();
                                }
                                else {
                                    key.interestOps( change.ops );
                                }
                        }
                    }
                    changeRequests.clear();
                }

                synchronized (pendingData) {
                    if (shutdownRequested && pendingData.isEmpty()) {
                        break;
                    }
                }
                // Wait for an event one of the registered channels
                selector.select();

                // Iterate over the set of keys for which events are available
                final Iterator<SelectionKey> selectedKeys = selector.selectedKeys().iterator();
                while (selectedKeys.hasNext()) {
                    final SelectionKey key = selectedKeys.next();
                    selectedKeys.remove();

                    if (!key.isValid()) {
                        continue;
                    }

                    // Check what event is available and deal with it
                    if (key.isAcceptable()) {
                        accept( key );
                    }
                    else if (key.isReadable()) {
                        read( key );
                    }
                    else if (key.isWritable()) {
                        write( key );
                    }
                }
            }

        }
        catch (final Exception e) {
            throw new RuntimeException( e );
        }

    }

    public void sendMessage( ClientEntry client, Message m ) {

        final ByteArrayOutputStream bo = new ByteArrayOutputStream();
        try {
            final ObjectOutputStream os = new ObjectOutputStream( bo );
            os.writeObject( m );
        }
        catch (final IOException e) {
            throw new RuntimeException( e );
        }

        final int size = bo.size();
        final ByteBuffer bb = ByteBuffer.allocate( size + Integer.SIZE / 8 );

        bb.putInt( size );
        bb.put( bo.toByteArray() );
        bb.rewind();
        synchronized (changeRequests) {
            // Indicate we want the interest ops set changed
            final SocketChannel socket = findSocketForClient( client );
            changeRequests.add( new ChangeRequest( socket, ChangeRequest.CHANGEOPS, SelectionKey.OP_WRITE ) );

            // And queue the data we want written
            synchronized (pendingData) {
                List<ByteBuffer> queue = pendingData.get( socket );
                if (queue == null) {
                    queue = new ArrayList<ByteBuffer>();
                    pendingData.put( socket, queue );
                }
                queue.add( bb );
            }
        }

        // Finally, wake up our selecting thread so it can make the required changes
        selector.wakeup();
    }

    public void shutdown() {
        shutdownRequested = true;

    }

    private void write( SelectionKey key ) throws IOException {
        final SocketChannel socketChannel = (SocketChannel) key.channel();

        synchronized (pendingData) {
            final List<ByteBuffer> queue = pendingData.get( socketChannel );

            // Write until there's not more data ...
            while (!queue.isEmpty()) {
                final ByteBuffer buf = queue.get( 0 );
                final int size = socketChannel.write( buf );
                if (size < 0) {
                    LOGGER.warning( "Socket returned -1 on write attempt" );
                }
                if (buf.remaining() > 0) {
                    // ... or the socket's buffer fills up
                    break;
                }
                queue.remove( 0 );
            }

            if (queue.isEmpty()) {
                // We wrote away all data, so we're no longer interested
                // in writing on this socket. Switch back to waiting for
                // data.
                pendingData.remove( socketChannel );
                key.interestOps( SelectionKey.OP_READ );
            }
        }

    }
}
